// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
using System;
using System.Collections.Generic;
using System.Threading;
using KnessetCore.Dbl;
using KnessetCore.Members.Views;
using KnessetCore.Utils;
using log4net;

namespace KnessetCore.Members
{
	/// <summary>
	/// A class that encapsulates all knesset members operations. Acts as a singleton.
	/// </summary>
	public class KnessetMembers
	{
		#region Private variables.
		/// <summary>
		/// Holds members read from db as cache.
		/// </summary>
		private readonly Dictionary<string, KnessetMember> members = new Dictionary<string, KnessetMember>();
		/// <summary>
		/// Will lock refresh method while writing.
		/// </summary>
		static readonly ReaderWriterLock rwl = new ReaderWriterLock();

		/// <summary>
		/// Log data.
		/// </summary>
		private readonly ILog logger = LogManager.GetLogger("KnessetCore.Members");
		#endregion

		#region Singleton implementation.
		private KnessetMembers()
		{
			RefereshMembers();
		}

		private static readonly KnessetMembers instance = new KnessetMembers();
		/// <summary>
		/// Gets singleton object.
		/// </summary>
		public static KnessetMembers GetInstance()
		{
			return instance;
		} 
		#endregion

		#region Public interface
		/// <summary>
		/// Add new knesset member.
		/// </summary>
		/// <param name="member">Member to add</param>
		/// <exception cref="ArgumentException">Member with the same name already exists.</exception>
		/// <exception cref="KnessetMembersException">Error while adding member to db.</exception>
		/// <exception cref="ArgumentNullException">Name argument is null.</exception>
		public void Add(MemberView member)
		{
			logger.Debug("~~~~~Entered KnessetMembers.Add");
			logger.Info("Name to be added: " + member);
			if (member == null)
			{
				throw new ArgumentNullException("name is null");
			}
			try
			{
				rwl.AcquireWriterLock(Timeout.Infinite);
				// Make sure that member does not exist.
				if (members.ContainsKey(member.Name))
				{
					logger.Error("Member already exists.");
					throw new ArgumentException("Member already exists.");
				}
				// Add a new member to the db.
				int affected = Constants.Members.Insert(member.Name, member.KnessetLink, member.ImgLink);
				// Make sure that the number of affected rows is not 0 which means success.
				if (affected == 0)
				{
					logger.Error(string.Format("Unable to add member to db. Member name is: {0}", member));
					throw new KnessetMembersException(string.Format("Unable to add member to db. Member name is: {0}", member));
				}
				else
				{
					logger.Info(string.Format("Added new member to db: {0}", member));
					// Add new member to cache.
					int id = Constants.Members.GetIdByName(member.Name)[0].ID;
					try
					{
						// Create new member.
						KnessetMember singleMember = new KnessetMember(id, member.Name, member.ImgLink, member.KnessetLink);
						members.Add(member.Name, singleMember);
						logger.Info(string.Format("Added new member to map: {0}", member));
					}
					catch (ArgumentOutOfRangeException e)
					{
						logger.Error(string.Format("Unable to add member to local cache: {0}", e.Message));
						throw new KnessetMembersException(e.Message, e);
					}
				}
				
			}
			finally
			{
				rwl.ReleaseWriterLock();
			}
			logger.Debug("~~~~~Exited KnessetMembers.Add");
			
		}

		/// <summary>
		/// Reload members data from the persistent location to the cache.
		/// </summary>
		/// <exception cref="KnessetMembersException">Error while reading values from persistent location.</exception>
		public void RefereshMembers()
		{
			
			logger.Debug("~~~~~Entered KnessetMembers.RefereshMembers");
			rwl.AcquireWriterLock(Timeout.Infinite);
			// Clear map.
			members.Clear();
			logger.Info("Clearing cache.");
			rwl.ReleaseWriterLock();
			try
			{
				rwl.AcquireWriterLock(Timeout.Infinite);
				// Reload values from db.
				Knesset.ParliamentMembersDataTable data = Constants.Members.GetData();
				foreach (Knesset.ParliamentMembersRow row in data)
				{
					// Create new member.
					KnessetMember singleMember = new KnessetMember(row.ID, row.Name, row.Img, row.Link);
					logger.Info(string.Format("Created new member from db: {0}", singleMember));
					// We're updating the cache make sure to lock.
					members.Add(row.Name, singleMember);
				}
			}
			catch (Exception e)
			{
				logger.Error(string.Format("Error while updating members from DB: {0}\n{1}", e.Message, e.StackTrace));
				Constants.CriticalErrorLogger.Info(string.Format("Error while updating members from DB: {0}\n{1}", e.Message, e.StackTrace));
				throw new KnessetMembersException(e.Message, e);
			}
			finally
			{
				rwl.ReleaseWriterLock();
			}
			// Check how many members do we have ?
			if (members.Keys.Count > Constants.NUMBER_OF_KNESSET_MEMBERS)
			{
				logger.Error("Number of knesset members is above 120.");
				Constants.CriticalErrorLogger.Info("Number of knesset members is above 120.");
			}
			logger.Debug("~~~~~Exited RefereshMembers");
		}

		/// <summary>
		/// Gets a member by its name.
		/// </summary>
		/// <param name="name">Name of the member.</param>
		/// <returns>Knesset member found.</returns>
		/// <exception cref="KeyNotFoundException">Member does not exist.</exception>
		/// <exception cref="ArgumentNullException">Name argument is null.</exception>
		public KnessetMember GetByName(string name)
		{
			logger.Debug("~~~~~Entered KnessetMembers.GetByName");
			if (name == null) throw new ArgumentNullException("name is null.");
			KnessetMember returnValue = null;
			try
			{
				rwl.AcquireReaderLock(Timeout.Infinite);
				// Make sure that member exists.
				returnValue = members[name];
				if (!members.ContainsKey(name))
				{
					throw new KeyNotFoundException("Member does not exist.");
				}
				logger.Info(string.Format("Name requested: {0}", name));
			}
			finally
			{
				rwl.ReleaseReaderLock();
			}
			logger.Debug("~~~~~Exited KnessetMembers.GetByName");
			return returnValue;
			
		}

		/// <summary>
		/// Remove a member by its name.
		/// </summary>
		/// <param name="name">Name of the member.</param>
		/// <exception cref="ArgumentNullException">Name argument is null.</exception>
		/// <exception cref="KnessetMembersException">Error while removing values from persistent location.</exception>
		/// <exception cref="KeyNotFoundException">Member does not exist.</exception>
		public void RemoveByName(string name)
		{
			logger.Debug("~~~~~Entered KnessetMembers.RemoveByName");
			if (name == null) throw new ArgumentNullException("name is null.");
			try
			{
				rwl.AcquireWriterLock(Timeout.Infinite);
				logger.Info(string.Format("Name to remove: {0}", name));
				// Check that member exists.
				if (!members.ContainsKey(name))
				{
					logger.Error("Member does not exist.");
					throw new KeyNotFoundException("Member does not exist.");
				}
				// Get id of member.
				int id = members[name].Id;

				// Remove from db.
				int affected;
				try
				{
					affected = Constants.Members.DeleteQuery(name);
				}
				catch (Exception e)
				{
					throw new KnessetMembersException("Unable to remove member from table.", e);
				}
				if (affected != 1)
				{
					throw new KnessetMembersException("Delete query affected more then one row: " + affected);
				}
				try
				{
					// Now remove any entries it had in other tables.
					removeFromHours(id);
				}
				catch (Exception e)
				{
					throw new KnessetMembersException("Unable to remove member from hours table.", e);
				}
				members.Remove(name);
				
			}
			finally
			{
				rwl.ReleaseWriterLock();
			}
			logger.Debug("~~~~~Exited KnessetMembers.RemoveByName");
		}



		/// <summary>
		/// Checks whether member exists or not.
		/// </summary>
		/// <param name="name">Name of member.</param>
		/// <returns>True if member exists, false otherwise.</returns>
		/// <exception cref="ArgumentNullException">Name argument is null.</exception>
		public bool IsExist(string name)
		{
			logger.Debug("~~~~~Entered KnessetMembers.IsExist");
			if (name == null) throw new ArgumentNullException("name");
			bool result;
			try
			{
				
				rwl.AcquireReaderLock(Timeout.Infinite);
				result = members.ContainsKey(name);
				logger.Info(string.Format("Member: {0}IsExist: {1}", name, result));
				
			}
			finally
			{
				rwl.ReleaseReaderLock();
			}
			logger.Debug("~~~~~Exited KnessetMembers.IsExist");
			return result;
			
		}
		/// <summary>
		/// Retreive all the available members.
		/// </summary>
		/// <returns>A collection of available members.</returns>
		public ICollection<KnessetMember> GetMembers()
		{
			logger.Debug("~~~~~Entered KnessetMembers.GetMembers");
			rwl.AcquireReaderLock(Timeout.Infinite);
			// Make a defensive copy to make sure that the data is static, i.e. even if values are deleted
			// from the members list the return value will always stay the same.
			ICollection<KnessetMember> returnValue = new LinkedList<KnessetMember>(members.Values);
			rwl.ReleaseReaderLock();
			logger.Debug("~~~~~Exited KnessetMembers.GetMembers");
			// Make a copy and return it.
			return returnValue;
		}
		/// <summary>
		/// Merge between two members, that is move all data belonging to one member to the other one.
		/// Need to suttesify the following constraints:
		/// 1. 'from' != 'to'
		/// 2. 'from' and 'to' exists
		/// </summary>
		/// <param name="from">Merge from this member.</param>
		/// <param name="to">Merge to this member.</param>
		/// <returns>How many merge operations occured. 0 means no merge occured.</returns>
		/// <exception cref="KnessetMembersException">Error while trying to merge.</exception>
		public int MergeMembers(KnessetMember from, KnessetMember to)
		{
			logger.Debug("Entered KnessetMembers.MergeMembers");
			int returnValue = 0;
			if (from == null) throw new ArgumentNullException("from");
			if (to == null) throw new ArgumentNullException("to");
			
			logger.Info("From: " + from + ", To: " + to);
			// Check that they are not the same.
			if (from.Id == to.Id)
			{
				logger.Error("From == To.");
				throw new KnessetMembersException("Merge from member equals merge to member.");
			}
			// First check that both patients exist.
			if (IsExist(from.Name) && IsExist(to.Name))
			{
				rwl.AcquireWriterLock(Timeout.Infinite);
				// When merging you have to copy the whole patient's information from associated tables to the
				// other patient and then delete the redundant patient.
				// There is a possible bug in this code, What happens if we have the same dates ?
				int affected; 
				try
				{
					// Merge hours information.
					affected = Constants.Hours.MergeHours(to.Id, from.Id);
					
				}
				catch (Exception e)
				{
					throw new KnessetMembersException("Unable to merge: " + e.Message, e);
				}
				finally
				{
					// Currently we only have time information.
					rwl.ReleaseWriterLock();
				}
				// If nothing was affected then ignore the merge, something wrong went here.
				if (affected == 0)
				{
					logger.Info("No merge occured, did not delete: " + from);
					
				}
				else
				{
					logger.Info("Removing member " + from);
					// Now delete the old member.
					RemoveByName(from.Name);
				}
				returnValue = affected;
			}
			else
			{
				logger.Error("One of the knesset members does not exist.");
				throw new MemberDoesNotExistException("One of the knesset members does not exist.");
			}
			logger.Debug("Exited KnessetMembers.MergeMembers");

			return returnValue;
		}
		#endregion

		#region Private interface
		/// <summary>
		/// Remove all rows that contain this member.
		/// </summary>
		/// <param name="id">Id of member to remove.</param>
		/// <returns>Number of rows affected.</returns>
		private static int removeFromHours(int id)
		{
			return Constants.Hours.DeleteById(id);
		} 
		#endregion
	}
}